虚拟 DOM
virtual DOM 是将真实的 DOM 的数据抽取出来,以对象的形式模拟树形结构。比如 dom 是这样的:
本质就是在 JS 和 DOM 之间做了一个缓存
<div>
<p>123</p>
</div>对应的 virtual DOM(伪代码):
var Vnode = {
tag: 'div',
children: [
{ tag: 'p', text: '123' }
]
};所谓虚拟 DOM 其实只是一个包含了标签类型 type,属性 props 以及它包含子元素 children 的 js 对象
虚拟 DOM 好处
- 是以 javascript 对象为基础而不依赖真实平台环境,具有跨平台的能力
- 浏览器平台渲染 DOM
- 服务端渲染 SSR (Nuxt.js/Next.js)
- 原生应用 (Weex/React Native)
- 小程序 (mpvue/uni-app) 等
- 复杂视图情况下提升渲染性能(原生 DOM 操作慢,可以将 DOM 操作放在 JS 层,利用
patch等算法计算真正需要更新的节点,减少 DOM 操作,提高效率) - 维护视图和状态的关系(虚拟 dom 可以很好地跟踪当前 dom 状态,因为它会根据当前数据生成一个描述当前 dom 结构的虚拟 dom,然后数据发生变化时,有生成一个新的虚拟 dom,而两个虚拟 dom 恰好保存了变化前后的状态)
key 的作用
在组件进行 diff 时作为唯一标识
用 index 作为 key 的问题
- 若对数据进行:逆序添加、逆序删除等破防顺序的操作:
- 会产生没有必要的真实 DOM 更新 ——> 界面效果没问题,但效率低
- 如果结构中还包含输入类的 DOM:
- 会产生错误 DOM 更新 ——> 界面有问题
- 仅用于列表展示,使用 index 作为 key 是没有问题的
Diff 算法
彻底搞懂 Vue 把虚拟 dom(vnode)转化为真实 dom 的过程 (diff.js)

对同层的树节点进行比较(由上至下,层层对比),时间复杂度 O(n)
比较点:
- 比较节点类型和节点属性是否相同
- 再比较子节点或者文本节点是否相同
- 文本节点先比较节点内容类型(String 还是 Number)
- 子节点比较调用
patchVNode 方法
以上出现一个不匹配则判断节点不同,直接用 VNode 替换
patchVnode 方法 :
- oldVnode 有子节点,Vnode 没有
- oldVnode 没有子节点,Vnode 有
- 都只有文本节点
- 都有子节点(调用
updateChildren 方法)
updateChildren 方法 (子节点更新策略)
子节点更新策略
四种命中查找,由上至下依次命中:(待查找的节点的头尾节点指针)
- 新前和旧前(相同则双方指针下移,重新比较,否则 判断下移)
- 新后和旧后(相同则双方指针上移动)
- 新后和旧前(相同则新后指针上移,旧前指针下移)
- 新前和旧后(相同则新前指针下移,旧后指针上移)
循环的条件: while(新前 <= 新后 && 旧前 <= 旧后)
循环结束后:
- 如果新节点存在节点,则新增节点
- 如果旧节点存在节点,则删除
旧前和旧后间的节点
第三种情况发送
旧节点设为 undefined 新前指向的节点,复制到旧后之后,
第四种情况发生
旧节点设为 undefined,新节点指向的节点,复制到旧前之前
四种情况都没发生
判断 新前指针指向的节点在 old 中有没有
有就将该节点插入到 oldStart 前面,将 old 中该节点值设置为 undefined,newStart 指针 下移
如果没有就将该节点插入到 old 之前,newStart 指针 下移
patch 函数
两个作用:
- 根据 VNode 挂载 DOM
- 根据新旧 vnode 更新 DOM
patch 函数入参。
- 第一个参数 n1 表示旧的 vnode,当 n1 为 null 的时候,表示是一次挂载的过程
- 第二个参数 n2 表示新的 vnode 节点,后续会根据这个 vnode 进行相关的处理
- 第三个参数 container 表示 DOM 容器,也就是 vnode 渲染生成 DOM 后,会挂载到 container 下面
vue2 和 vue3 diff 算法区别
vue2 的双端比较
vue3 的最长递增子序列
- 从前往后对比 (头部与头部比较)
- 从后往前对比 (尾部与尾部比较)
- 基于最长子序列的比较进行 -》移动|添加|删除
举个例子:
旧数组:【a,b,c,d,e,f,g】
新数组:【a, b, f, c, d, e, h, g]
**首先是开始最简单的比较获取之前的缓存数据:**
1. 首先是头头对比,发现不同就停止本次循环【a,b】
2. 然后是尾尾比较得到【g】;
**经过1.2的比较后得到不同局部数据【f,c,d,e,h】,接下来就是不同数据的比较,这一步就是要使用最长子序列方法进行(移动|添加|删除)的操作。**
**在源码中使用map函数对数组的值和key进行一个绑定。**(有兴趣的同学可以看下源码)
源码中通过 newIndexToOldIndexMap 拿到在数组里对应的下标,生成数组 [ 5, 2, 3, 4, -1 ],-1 是老数组里没有的,没有的数据我们只能往旧节点补充数据。
很明显可以发现在[ 5, 2, 3, 4, -1 ]中【2,3,4】是有序的,**并且是依次递增的,此时他对应的节点是【c,d,e】,这个时候我们就很容易的保持【2,3,4】不变进行其他数据的移位,既是在此基础的前方插上一位就能完成新节点的更新**vue3 中对 diff 算法的优化
- 节点标记为动态和静态节点
- 数组循环算法调整为 最小递增子序列
- 事件缓存
vue2 中的虚拟 dom 是进行全量的对比,当页面数据发生变更后,会判断虚拟 don 所有的节点有没有发生变化
针对这一点,vue3 在创建新的虚拟 dom 树的时候,会在动态标签(这个 dom 中的内容可能会发生变化)后添加一个静态标记,在与上次虚拟 dom 树比较时就只对比这些带有静态标记的节点
事件缓存
比如这样一个有点击事件的按钮
<button @click="handleClick">按钮</button>
复制代码来看下在 Vue3 被编译后的结果
export function render(_ctx, _cache, $props, $setup, $data, $options) {
return (_openBlock(), _createElementBlock("button", {
onClick: _cache[0] || (_cache[0] = (...args) => (_ctx.handleClick && _ctx.handleClick(...args)))
}, "按钮"))
}VNode 生成真实节点树
彻底搞懂 Vue 把虚拟 dom(vnode)转化为真实 dom 的过程 (render.js)

通过 render 将 VNode 生产 DOM,涉及三个方法 render,createRealElement 以及 setProperties
主要进行以下步骤:
- 判断 VNode 类型,如果是组件类型,则挂载组件(patch 方法)
- 创建组件实例
- 渲染组件子树(调用 render 函数得到一个子树的 VNode)
- 挂载到 container 中(DOM 容器,也就是 vnode 渲染生成 DOM 后,会挂载到 container 下面)
2.如果是普通元素,也调用 patch 方法 - 创建 DOM 元素节点
- 处理 props 属性
- 处理文本或者数组的子节点
- 递归 patch 子节点(深度优先遍历树的方式)
流程图如下:
参考
官网: API — Vue.js